Перейти к основному содержимому

5.05. Справочник по C#

Разработчику Архитектору

📚 Лексика, типы, операторы, ключевые слова

1. Типы данных

Встроенные (примитивные) типы и их псевдонимы:

C# псевдонимСистемный типРазмер (бит)Диапазон / Примечание
boolSystem.Boolean8true / false
sbyteSystem.SByte8−128 … 127
byteSystem.Byte80 … 255
shortSystem.Int1616−32 768 … 32 767
ushortSystem.UInt16160 … 65 535
intSystem.Int3232−2 147 483 648 … 2 147 483 647
uintSystem.UInt32320 … 4 294 967 295
longSystem.Int6464−2⁶³ … 2⁶³−1
ulongSystem.UInt64640 … 2⁶⁴−1
nintSystem.IntPtr32/64зависит от архитектуры (указательный размер)
nuintSystem.UIntPtr32/64зависит от архитектуры
charSystem.Char16UTF-16 символ (один элемент кодировки)
floatSystem.Single32~±1.5 × 10⁻⁴⁵ … ±3.4 × 10³⁸, 7 цифр точности
doubleSystem.Double64~±5.0 × 10⁻³²⁴ … ±1.7 × 10³⁰⁸, 15–16 цифр
decimalSystem.Decimal128±1.0 × 10⁻²⁸ … ±7.9 × 10²⁸, 28–29 цифр, для финансовых вычислений
stringSystem.Stringrefссылка на неизменяемую UTF-16 строку
objectSystem.Objectrefкорневой тип всех ссылочных и упакованных значений

Категории типов:

  • Значимые (value types): структуры (struct), перечисления (enum), простые типы (int, DateTime, Guid, Span<T>, UnsafePointer, nint).
  • Ссылочные (reference types): классы (class), интерфейсы (interface), делегаты (delegate), массивы, строки — все наследуются от System.Object.
  • Указатели (T*): доступны только в unsafe-контексте.

Специальные типы / конструкции:

КонструкцияОписание
dynamicОтложенное связывание: проверка членов происходит во время выполнения. Наследуется от System.Object, но компилятор отключает статическую проверку.
varНе тип, а указание компилятору вывести тип на основе инициализатора. Работает только с локальными переменными.
recordСинтаксический сахар для неизменяемых (по умолчанию) типов с генерацией Equals, GetHashCode, ToString, операторов ==/!=, with и (в record struct) copy-конструкторов.
record structЗначимый record, появился в C# 10.
ref structЗначимый тип, который не может покидать стек: не может быть полем класса, элементом массива, кэширован в async, упакован в object. Примеры: Span<T>, ReadOnlySpan<T>.
required (C# 11)Модификатор свойства или поля в record/class/struct, требующий обязательной инициализации либо в инициализаторе объекта, либо в конструкторе.
initМодификатор метода-сеттера: допускает присваивание только во время инициализации объекта (в конструкторе или инициализаторе).
readonly (для struct)Гарантирует, что экземпляр структуры не будет изменён после инициализации — все поля readonly, методы readonly.
readonly refВозвращаемое значение ссылки на readonly-данные — запрещает их модификацию.
scoped (C# 12)Указывает, что ссылка (в т.ч. ref, Span<T>) не должна выходить за пределы текущего блока/метода.

2. Ключевые слова языка

Обязательные (не могут быть идентификаторами):

abstract, as, base, bool, break, byte, case, catch, char, checked, class, const, continue, decimal, default, delegate, do, double, else, enum, event, explicit, extern, false, finally, fixed, float, for, foreach, goto, if, implicit, in, int, interface, internal, is, lock, long, namespace, new, null, object, operator, out, override, params, private, protected, public, readonly, ref, return, sbyte, sealed, short, sizeof, stackalloc, static, string, struct, switch, this, throw, true, try, typeof, uint, ulong, unchecked, unsafe, ushort, using, virtual, void, volatile, while

Контекстные ключевые слова (могут быть идентификаторами в других контекстах):

add, alias, and, ascending, async, await, by, descending, dynamic, equals, from, get, global, group, init, into, join, let, nameof, nint, not, nuint, on, or, orderby, partial, record, remove, required, select, set, unmanaged, value, var, when, where, with, yield

Примечание: global используется в global::System.Int32 для разрешения коллизий имён с помощью глобального пространства имён.


3. Модификаторы доступа

МодификаторПрименяется кВидимость
publicЛюбой членВезде
protectedЧлен классаВнутри содержащего класса и производных классов
internalЛюбой членВ пределах сборки (assembly)
protected internalЧлен классаВнутри сборки или в производных классах (в любой сборке)
privateЛюбой членТолько внутри содержащего типа
private protectedЧлен классаТолько в производных классах внутри той же сборки

Уровень по умолчанию:

  • Для class/struct: internal
  • Для членов: private

4. Модификаторы членов (не доступа)

МодификаторПрименениеЭффект
staticкласс, метод, поле, свойство, событие, конструктор, операторПринадлежит типу, а не экземпляру
readonlyполе, structПоле инициализируется только в объявлении или конструкторе; readonly struct — все поля readonly, все методы readonly
constполеКомпилируемая константа (только для bool, целых, char, string, enum). Подставляется в IL как литерал.
virtualметод, свойство, событие, индексаторРазрешает переопределение в производном классе
overrideметод, свойство, событие, индексаторЯвное переопределение virtual/abstract члена
abstractкласс, метод, свойство, событие, индексаторТип или член без реализации — должен быть переопределён в неабстрактном классе
sealedкласс, метод (override), recordКласс — запрет на наследование; метод — запрет дальнейшего переопределения
partialclass, struct, interface, методРазделение определения на несколько файлов или частей (требуется одинаковое имя и сигнатура)
externметодРеализация вынесена за пределы кода (P/Invoke, DllImport)
unsafeблок, метод, типВключение режима указателей и небезопасных операций
volatileполеЗапрещает компилятору и CPU применять оптимизации, нарушающие порядок чтения/записи (для многопоточной синхронизации без lock)
asyncметод, локальная функцияУказывает, что метод может содержать await и возвращает Task/Task<T>/ValueTask/ValueTask<T>/IAsyncEnumerable<T>/void (только для event handlers)
refпараметр, возвращаемое значение, локальная переменная, structПередача/возврат по ссылке (не копируется значение)
inпараметрref readonly — передача по ссылке без возможности модификации
outпараметрВыходной параметр: инициализация обязательна в методе, не читается до присвоения
paramsпоследний параметр массиваПозволяет вызывать метод с переменным числом аргументов (например, params int[] values)
requiredсвойство, поле (в C# 11+)Обязательная инициализация при создании объекта
scopedпараметр (ref, Span<T> и др.)Гарантия, что ссылка не покинет текущую область

5. Операторы

Приоритет (уменьшается сверху вниз):

ПриоритетОператорыПримечания
20x.y, x?.y, f(x), a[x], x++, x--, new, typeof, checked, unchecked, default, nameof, sizeof, stackalloc, x->y, x! (null-forgiving), x is T, x as TПостфиксные, вызовы, доступ по индексу, создание
19+x, -x, !x, ~x, ++x, --x, ^x (index from end), await, &x, *x, (T)xУнарные, приведения, указательные операции
18x * y, x / y, x % yМультипликативные
17x + y, x - yАддитивные
16x << y, x >> yПобитовые сдвиги
15x < y, x <= y, x > y, x >= y, is, asСравнение, проверка типа
14x == y, x != yРавенство
13x & yПобитовое И (для bool — несокращённое логическое И)
12x ^ yПобитовое исключающее ИЛИ (XOR)
11x | yПобитовое ИЛИ (для bool — несокращённое логическое ИЛИ)
10x && yСокращённое логическое И
9x || yСокращённое логическое ИЛИ
8x ?? yNull-coalescing
7c ? a : bТернарный условный
6x = y, x op= y, x ??= y, x ?= y (нет), => — не операторПрисваивание (все возвращают присвоенное значение)
5yield return, yield breakТолько в итераторах (IEnumerable<T>, IAsyncEnumerable<T>)
4=>Лямбда-выражение (не входит в приоритет таблицы как оператор)

Специальные операторы и конструкции:

Оператор / КонструкцияОписание
isПроверка типа, сопоставление с образцом: obj is string s, x is > 0 and < 10, x is int[] { Length: 5 }
asБезопасное приведение: возвращает null, если несовместимо (только для ссылочных типов и Nullable<T>)
??Null-coalescing: a ?? b → если a null, то b, иначе a
??=Null-coalescing assignment: a ??= ba = a ?? b
?. / ?[]Условный оператор доступа: obj?.Prop?[0]?.Method() — прерывает цепочку при null
!Null-forgiving (suppression) operator — отключает предупреждения NRT (nullable reference types)
^Index from end: arr[^1] — последний элемент; ^n = Length - n (требуется using System;)
..Range operator: arr[1..^1] — срез от индекса 1 до предпоследнего; ..3, 2.., ..
()Вызов, группировка, приведение, кортеж
[]Доступ по индексу, объявление массива, атрибуты
{}Инициализатор объекта, коллекции, анонимного типа, блок кода
=>Expression-bodied member, лямбда
nameof(...)Получение имени идентификатора как string (без hardcode) — работает на этапе компиляции
default(T) / defaultЗначение по умолчанию для типа: 0, false, null, обнуленная структура
sizeof(T)Размер типа в байтах — только для неуправляемых типов и в unsafe или Unsafe.SizeOf<T>() в безопасном коде
stackalloc T[n]Выделение массива на стеке (только в unsafe или для Span<T> в безопасном коде с ReadOnlySpan<byte> инициализаторами)
&x, *pАдрес и разыменование — только в unsafe

📚 Члены типов

1. Поля (Fields)

Синтаксис:

[<атрибуты>] [<модификаторы>] <тип> <имя> [= <инициализатор>];

Типы полей:

ТипПримерПримечание
Обычноеprivate int _count;Хранит состояние экземпляра
Статическоеpublic static readonly DateTime Epoch = new(1970, 1, 1);Принадлежит типу
readonlypublic readonly Guid Id = Guid.NewGuid();Инициализируется только в объявлении или конструкторе
constpublic const double Pi = 3.141592653589793;Компилируемая константа (только для примитивов и string)
volatileprivate volatile bool _isCancelled;Запрещает оптимизации, нарушающие порядок чтения/записи
required (C# 11)public required string Name;Обязательное к инициализации при создании объекта (new X { Name = "..." } или конструктор)
init (через field в C# 12)public string Name { get; init; } = field ?? "";field ссылается на неявное полеТолько в свойствах с init или get/set
ref struct полеЗапрещеноref struct не может быть полем обычного класса/структуры
ref полеНельзя объявить напрямую, но можно через unsafe-указатель или ref-параметр/локальную переменную
fixed (буфер)public unsafe fixed byte Buffer[256];Только в unsafe, только в struct, выделяется инлайн

Инициализация:

  • Порядок инициализации:
    1. Статические поля → статический конструктор
    2. Поля экземпляра → конструктор базового класса (base(...)) → конструктор текущего класса
  • required поля не инициализируются автоматически — контроль на этапе компиляции (если не указано при создании, ошибка CS9035).

2. Свойства (Properties)

Синтаксис:

[<атрибуты>] [<модификаторы>] <тип> <Имя>
{
[<модификаторы>] get { <тело> }
[<модификаторы>] set { <тело> }
[<модификаторы>] init { <тело> }
}
// Или expression-bodied:
public string Name => _name;
public int Length => _items?.Length ?? 0;

Виды свойств:

ТипПримерПримечание
Автосвойствоpublic string Name { get; set; }Компилятор генерирует скрытое поле <Name>k__BackingField
init-свойствоpublic string Id { get; init; }Присваивание только при инициализации (new X { Id = "..." }, первичный конструктор, with)
required-свойствоpublic required string Login { get; set; }Обязательное заполнение при создании — иначе ошибка компиляции
readonly автосвойствоpublic string Version { get; } = "1.0";Только get; инициализация — в конструкторе или при объявлении
virtual/abstract/overridepublic virtual int Count => _items.Count;Поддерживает переопределение
Выражениеpublic bool IsEmpty => Count == 0;Краткая форма для get
Полное (с логикой)private string _name; public string Name { get => _name; set => _name = value?.Trim() ?? ""; }Контроль доступа и валидации
Индексаторpublic T this[int index] => _items[index];Свойство с параметром — имя всегда this

Ключевые слова в set/init:

  • value — неявный параметр, тип совпадает с типом свойства.
  • field (C# 12) — ссылка на неявное поле автосвойства (только в get/set/init, не в обычных свойствах с явным полем):
    public string Name 
    {
    get => field?.ToUpper();
    init => field = value?.Trim() ?? "";
    }

3. События (Events)

Синтаксис:

[<атрибуты>] [<модификаторы>] event <делегат> <Имя>;
// Или явная реализация:
private EventHandler<MyEventArgs> _myEvent;
public event EventHandler<MyEventArgs> MyEvent
{
add => _myEvent += value;
remove => _myEvent -= value;
}

Стандартные делегаты:

ДелегатСигнатураПрименение
EventHandlervoid Method(object sender, EventArgs e)Без данных события
EventHandler<T>void Method(object sender, T e) where T : EventArgsС данными (T наследует EventArgs)
Пользовательскийdelegate void MyDelegate(string msg);Редко — предпочтительно Action<T>/Func<T,R> или EventHandler<T>

Правила:

  • Внутри класса событие вызывается как делегат: MyEvent?.Invoke(this, args);
  • За пределами — только += и -= (компилятор запрещает прямой вызов и присваивание).
  • event-поле нельзя явно инициализировать (= null избыточно — по умолчанию null).
  • В многопоточной среде рекомендуется копировать делегат перед вызовом:
    var handler = MyEvent;
    handler?.Invoke(this, args);
    Или использовать Interlocked.CompareExchange / Volatile.Read.

4. Методы (Methods)

Синтаксис:

[<атрибуты>] [<модификаторы>] <возвращаемый тип> <Имя>([<параметры>])
{
<тело>
}
// Или expression-bodied:
public int Square(int x) => x * x;

Параметры:

Способ передачиСинтаксисПоведение
По значению (по умолчанию)void M(int x)Копия значения (для ссылочных — копия ссылки)
refvoid M(ref int x)Передача по ссылке — изменения влияют на аргумент
outbool TryParse(string s, out int result)Выходной параметр — должен быть проинициализирован в методе
invoid M(in ReadOnlySpan<byte> data)ref readonly — нельзя модифицировать, но избегается копирование
paramsvoid Log(params string[] messages)Последний параметр массива; вызов: Log("a", "b", "c")
По умолчаниюvoid M(int x = 10)Значение подставляется, если аргумент не указан
ИменованныйM(x: 5, y: "test")Порядок не важен; сочетается с params (Log(severity: Info, messages: "a", "b"))

Особенности методов:

ВидПримерПримечание
Статическийpublic static int Max(int a, int b) => Math.Max(a, b);Нет this
Виртуальныйpublic virtual void Draw() { ... }Может быть переопределён
Абстрактныйpublic abstract void Execute();Только в abstract-классе, без тела
Переопределённыйpublic override string ToString() => $"Point({X}, {Y})";Должен соответствовать сигнатуре базового virtual/abstract
Запечатанныйpublic sealed override void Draw() { ... }Запрещает дальнейшее переопределение
Частичныйpartial void Validate();Может быть определён в другой части partial class; если не реализован — компилятор удаляет вызовы
Внешнийpublic static extern int GetTickCount(); [DllImport("kernel32")] ...Реализация в нативном коде
Асинхронныйpublic async Task<int> DownloadAsync()Должен содержать await или возвращать Task/ValueTask/IAsyncEnumerable
Итераторныйpublic IEnumerable<int> Range(int start, int count)yield return i;Компилятор генерирует IEnumerator<T>
Локальная функцияvoid Outer() { int Helper() => 42; }Доступна только в методе; может быть static, async, iterator
Обобщённыйpublic T FirstOrDefault<T>(IEnumerable<T> source, Func<T, bool> pred)Параметры типа: where T : class, struct, new(), IDisposable, notnull, unmanaged

Generic constraints (ограничения параметров типа):

ОграничениеПримерЗначение
where T : classclass Box<T> where T : classСсылочный тип (включая string, делегаты, массивы)
where T : class?(C# 9+, NRT)Ссылочный тип, допускает null
where T : structSpan<T> where T : structНеуправляемый значимый тип (не Nullable<T>)
where T : unmanaged(C# 7.3)Значимый тип без ссылок и GC-трекинга — можно использовать в unsafe
where T : new()T Create<T>() where T : new() => new T();Должен иметь публичный конструктор без параметров
where T : IDisposablevoid Use<T>(T obj) where T : IDisposableДолжен реализовывать интерфейс
where T : notnull(C# 8+)Запрещает null даже в nullable-контексте
where T : Uvoid Copy<T, U>(T src, U dst) where T : UT должен быть U или производным от U

5. Конструкторы (Constructors)

Виды:

ТипСинтаксисПримечание
Экземпляраpublic Point(int x, int y) { X = x; Y = y; }Вызывается при new
Статическийstatic MyClass() { ... }Вызывается один раз перед первым использованием типа
Первичный (C# 12)public record Person(string Name, int Age) { ... }Сокращённая форма для record и class/struct — параметры становятся public readonly полями/свойствами
Делегированиеpublic Point() : this(0, 0) { }Цепочка через this(...) или base(...)

Поведение:

  • Если нет конструктора экземпляра — компилятор генерирует публичный без параметров (public C() : base() {}), кроме record, где генерируется первичный (если указаны параметры) или без параметров.
  • record с первичным конструктором автоматически создаёт:
    • Поля/свойства для параметров (по умолчанию public init для record class, public readonly для record struct)
    • Deconstruct
    • ToString, Equals, GetHashCode, ==, !=, оператор with
  • struct всегда имеет неявный конструктор без параметров (нельзя переопределить), но можно определить свой — при этом поля должны быть инициализированы до выхода.

6. Финализаторы (Finalizers)

~ClassName()
{
// Освобождение неуправляемых ресурсов
}
  • Компилируется в protected override void Finalize().
  • Вызывается GC асинхронно, не гарантируется порядок и момент.
  • Не использовать для управляемых ресурсов — только для IntPtr, хэндлов ОС.
  • Рекомендуется использовать IDisposable + SafeHandle вместо финализатора.
  • Если реализуете IDisposable, финализатор должен вызывать Dispose(false).

7. Операторы (Operators)

Перегружаемые:

ОператорСигнатураПримечание
Унарныеpublic static T operator +(T a)+, -, !, ~, ++, --, true, false
Бинарныеpublic static T operator +(T a, T b)+, -, *, /, %, &, |, ^, <<, >>, ==, !=, <, >, <=, >=
Приведенияpublic static explicit operator T(U u) public static implicit operator T(U u)explicit — требует явного приведения; implicit — не требует

Правила:

  • == и != должны быть перегружены парно.
  • Если перегружены ==/!=, то рекомендуется переопределить Equals и GetHashCode.
  • true/false — нужны для &&/\|\| (компилятор преобразует a && ba ? b : false при наличии true/false).
  • implicit приведение должно быть безопасным (без потерь, исключений).
  • Нельзя перегрузить: =, &&, \|\|, ??, ?., ->, ?:, new, typeof, sizeof, is, as, checked, unchecked.

8. Вложенные типы

public class Outer
{
private int _x;
public class Nested { /* имеет доступ к private Outer._x */ }
public struct NestedStruct { }
public interface INested { }
public delegate void NestedDelegate();
public enum NestedEnum { A, B }
}
// Использование: Outer.Nested, Outer.NestedStruct и т.д.
  • Доступность по умолчанию: private.
  • Вложенные типы наследуют generic-параметры внешнего типа:
    class Outer<T>
    {
    class Inner : IEnumerable<T> { ... } // T доступен
    }
  • private protected вложенный тип виден только в производных классах внутри той же сборки.

9. Частичные (partial) методы

// В одном файле:
partial void OnValidate();

// В другом:
partial void OnValidate()
{
// Если не реализован — вызовы удаляются компилятором
}
  • Должны возвращать void.
  • Могут иметь out-параметры, но не ref/in/params.
  • Могут быть static, но не virtual, abstract, override.
  • Используются в source generators (например, для hook-ов инициализации).

10. Записи (record, record struct)

Фичаrecord classrecord struct
Неизменяемость по умолчаниюinit-свойстваreadonly поля (если нет set)
with-выражениеДа (var y = x with { Name = "new" };)Да (копирует все поля)
Первичный конструкторrecord R(string Name);public string Name { get; init; }То же, но поля readonly
DeconstructАвтогенерируется: void Deconstruct(out string Name)Автогенерируется
Equals/GetHashCodeСравнение по значению полей/свойствТо же
==/!=ПерегруженыПерегружены
НаследованиеДа (но только от record или object)Нет (структуры не наследуются)
PrintMembersprotected virtual bool PrintMembers(StringBuilder sb)Нет (но можно определить)

📚 Управление памятью и производительность

1. Упаковка (boxing) и распаковка (unboxing)

Что происходит:

  • Boxing: преобразование значимого типа в object или интерфейс → выделение памяти в куче, копирование значения, возврат ссылки.
    int x = 42;
    object boxed = x; // boxing: new object { int m_value = 42 }
  • Unboxing: извлечение значения из упакованного объекта → проверка типа (isinst), копирование значения.
    int y = (int)boxed; // unboxing: проверка, что boxed — boxed int, затем копирование

Стоимость:

ОперацияСтоимостьПримечание
Boxing~100–300 тактовВыделение памяти, вызов GC.Alloc, копирование
Unboxing~10–30 тактовПроверка типа (RTTI), копирование
Повторное boxing одного и того же значенияНе кэшируется (кроме случаев ниже)object a = 5; object b = 5; ReferenceEquals(a, b)false

Кэширование значений (CLR-оптимизация):

Только для:

  • bool: true, false
  • char: U+0000U+007F (0–127)
  • Целые (sbyte, byte, short, ushort, int): значения от –128 до 127
    object a = 100, b = 100;
    Console.WriteLine(ReferenceEquals(a, b)); // true
    object c = 300, d = 300;
    Console.WriteLine(ReferenceEquals(c, d)); // false

Как избежать:

  • Использовать обобщённые коллекции (List<int>, а не ArrayList).
  • Избегать object/IComparable/IEquatable<object> для значимых типов.
  • Применять Span<T>, ref, in, ref struct.
  • В интерфейсах использовать generic-ограничения: where T : IEquatable<T>, а не IEquatable<object>.

2. IDisposable, IAsyncDisposable и управление ресурсами

Стандартный шаблон (Dispose pattern):

public class FileStreamWrapper : Stream, IDisposable
{
private FileStream? _stream;
private bool _disposed;

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Освободить управляемые ресурсы
_stream?.Dispose();
}
// Освободить неуправляемые (если есть напрямую — редко)
_stream = null;
_disposed = true;
}
}

public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this); // отменить финализацию
}

// Финализатор (только если есть неуправляемые ресурсы напрямую)
~FileStreamWrapper()
{
Dispose(disposing: false);
}

// IAsyncDisposable (для асинхронных ресурсов)
public async ValueTask DisposeAsync()
{
if (_stream != null)
{
await _stream.DisposeAsync().ConfigureAwait(false);
_stream = null;
}
_disposed = true;
GC.SuppressFinalize(this);
}
}

Ключевые правила:

ПравилоПримечание
Dispose() должен быть идемпотентнымПовторные вызовы — безопасны
GC.SuppressFinalize(this) — обязательно в Dispose()Иначе объект попадёт в финализационную очередь, замедляя GC
IAsyncDisposable должен возвращать ValueTask, а не TaskДля избежания аллокаций
await using → вызывает DisposeAsync()Аналог using, но для асинхронного освобождения
using var x = ...; — синтаксический сахарБлок using до конца текущей области видимости

Когда НЕ нужен финализатор:

  • Если ресурсы обёрнуты в SafeHandle (рекомендуется).
  • Если используются только управляемые ресурсы (Stream, HttpClient, SqlConnection и т.п.).

SafeHandle — предпочтительный способ:

public sealed class MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private MySafeHandle() : base(true) { }
protected override bool ReleaseHandle() => NativeMethods.CloseHandle(handle);
}

[DllImport("kernel32")]
public static extern MySafeHandle CreateFile(...);

→ Автоматически интегрируется с GC, thread-safe, предотвращает утечки при AppDomain unload.


3. Span<T>, ReadOnlySpan<T>, Memory<T>, ReadOnlyMemory<T>

Сравнительная таблица:

ТипГде может хранитьсяУправляемый?Може ли быть полем?Може ли выходить из метода?Использование
Span<T>стек, неуправляемая память, массивнетref struct❌ (кроме ref return)Быстрые операции в пределах метода
ReadOnlySpan<T>то женетref structТолько чтение
Memory<T>управляется GCПередача между асинхронными операциями
ReadOnlyMemory<T>то жеТолько чтение

Создание:

// Из массива
var arr = new byte[1024];
Span<byte> span = arr.AsSpan();
ReadOnlySpan<byte> roSpan = arr.AsSpan().Slice(10, 100);

// Из строки
ReadOnlySpan<char> strSpan = "Hello".AsSpan();

// Из неуправляемой памяти
unsafe
{
void* ptr = NativeMemory.Alloc(1024);
Span<byte> span = new(ptr, 1024);
NativeMemory.Free(ptr); // ← осторожно: span живёт дольше памяти!
}

// Из stackalloc (без unsafe в C# 7.2+)
Span<byte> buffer = stackalloc byte[256];

// Из Memory<T>
Memory<byte> mem = new byte[1024];
Span<byte> span = mem.Span; // только в синхронном контексте!

Ограничения Span<T>:

  • Нельзя:
    • быть полем класса/обычной структуры
    • быть элементом массива
    • использоваться в async/yield методах
    • передаваться в lambda, local function, try/catch/finally (в некоторых случаях)
    • использоваться как обобщённый аргумент (List<Span<T>> — ошибка)

→ Решение: использовать Memory<T> для передачи, .Span — для локальной обработки.

Пример безопасного использования:

public static bool TryParseHeader(ReadOnlySpan<byte> data, out int length)
{
if (data.Length < 4) { length = 0; return false; }
length = BinaryPrimitives.ReadInt32LittleEndian(data);
return length >= 0;
}

4. stackalloc, ArrayPool<T>, MemoryPool<T>

stackalloc:

Span<byte> buffer = stackalloc byte[1024]; // 1 KB на стеке
// или с размером во время выполнения:
int n = Math.Min(count, 1024);
Span<byte> temp = stackalloc byte[n];
  • Максимальный размер по умолчанию: ~1 МБ (ограничение Windows x64), но зависит от платформы.
  • Не вызывает GC, но переполнение стека ⇒ StackOverflowException.
  • В безопасном коде доступен только для Span<T> с T = byte, sbyte, char, ushort, short, uint, int, ulong, long, nint, nuint, float, double.

ArrayPool<T>.Shared:

var array = ArrayPool<byte>.Shared.Rent(2048);
try
{
// использовать array
}
finally
{
ArrayPool<byte>.Shared.Return(array, clearArray: false);
}
  • Пулы: глобальный (Shared) или пользовательский (ArrayPool<T>.Create(maxArrayLength, maxArraysPerBucket)).
  • clearArray: true — обнулить массив перед возвратом (защита от утечки данных).
  • Размеры: округляются до ближайшего бакета (256, 512, 1024, …, 1 048 576).

MemoryPool<T> (для Memory<T>):

var owner = MemoryPool<byte>.Shared.Rent(1024);
try
{
Memory<byte> mem = owner.Memory;
// использовать
}
finally
{
owner.Dispose(); // возвращает в пул
}
  • Особенно полезно для IAsyncEnumerable<T>, PipeWriter, ArrayPool-бэкенд.

5. System.Runtime.CompilerServices.Unsafe

Пакет: System.Runtime.CompilerServices.Unsafe (входит в .NET Standard 2.1+)

Основные методы:

МетодПодписьНазначение
As<TFrom, TTo>TTo As<TFrom, TTo>(ref TFrom source)Интерпретация битов одного типа как другого (без копирования)
AsPointervoid* AsPointer<T>(ref T value)Получение указателя на переменную (без unsafe)
AsRefref T AsRef<T>(void* source)Преобразование указателя в ref
SizeOf<T>int SizeOf<T>()Размер типа в байтах (без unsafe, работает для всех типов)
Add<T> / Subtract<T>ref T Add<T>(ref T source, int offset)Арифметика указателей через ref
Read<T> / Write<T>T Read<T>(void* source)Чтение/запись по неуправляемому адресу
CopyBlock / InitBlockvoid CopyBlock(void* dest, void* src, uint size)memcpy, memset на уровне IL

Примеры:

// Быстрое копирование без аллокаций
unsafe
{
byte* src = ...;
byte* dst = ...;
Unsafe.CopyBlock(dst, src, (uint)1024);
}

// Чтение int из байтового буфера без выравнивания
ReadOnlySpan<byte> data = ...;
int value = Unsafe.ReadUnaligned<int>(ref MemoryMarshal.GetReference(data));

// Обход ограничений generic-параметров
T CreateDefault<T>() => Unsafe.As<byte, T>(ref Unsafe.AsRef<byte>(null));
// (осторожно: может нарушить безопасность — только для value types с default layout)

Важно: Unsafe не отключает проверки границ массивов. Для этого — Unsafe.Add(ref arr[0], index) вместо arr[index].


6. Подсказки сборщику мусора и JIT

Атрибуты:

АтрибутПрименениеЭффект
[ThreadStatic]static полеКаждый поток — своя копия
[ThreadLocal]static поле (ThreadLocal<T>)То же, но с отложенной инициализацией
[MethodImpl(MethodImplOptions.AggressiveInlining)]МетодПодсказка JIT встраивать метод (не гарантируется)
[MethodImpl(MethodImplOptions.AggressiveOptimization)]МетодJIT применяет агрессивные оптимизации (даже в debug)
[MethodImpl(MethodImplOptions.NoInlining)]МетодЗапрет встраивания (для профилирования, security)
[MethodImpl(MethodImplOptions.NoOptimization)]МетодОтключить оптимизации (только для отладки)

Методы GC:

МетодПрименение
GC.KeepAlive(object)Продлить жизнь объекта до точки вызова (часто после P/Invoke)
GC.SuppressFinalize(object)Отменить финализацию (в Dispose())
GC.ReRegisterForFinalize(object)Повторно поставить в очередь финализации
GC.AllocateUninitializedArray<T>(int, bool)Выделить массив без инициализации default(T) (для struct, где default = 0)
GC.AllocateArray<T>(int, bool)Выделить массив, избегая GC pressure (в .NET 6+)
GC.GetGCMemoryInfo()Статистика по поколениям, паузам, фрагментации

JIT-подсказки через код:

  • Использовать ReadOnlySpan<T> вместо string/array для hot-путей.
  • Избегать виртуальных вызовов в циклах (JIT не может devirtualize без sealed или final).
  • Использовать ValueTask вместо Task для синхронного случая.
  • Размещать часто используемые поля вместе (cache-line locality).

7. ref struct, ref fields, scoped (C# 11–12)

ref struct — правила:

  • Может содержать только:
    • другие ref struct
    • ref T, ref readonly T
    • T*unsafe)
    • fixed-буферы
  • Нельзя:
    • реализовывать интерфейсы (даже IDisposable — но можно ref-расширения Dispose)
    • быть захваченным в замыкании
    • использоваться в async/yield
    • быть обобщённым параметром
    • быть возвращённым из метода (кроме ref return)

ref fields (C# 11):

public ref struct RefWrapper
{
public ref int Value; // разрешено с C# 11
}
  • Позволяет хранить ссылку на переменную внутри ref struct.
  • Требует осторожности: ссылка может стать "висячей".

scoped (C# 12):

Указывает, что ссылка не покидает текущую область:

// Запрещает возврат ссылки за пределы метода
public static scoped ref int Max(scoped ref int a, scoped ref int b)
=> ref (a > b ? ref a : ref b);

// Запрещает захват в замыкание
public static void Process(scoped Span<byte> data)
{
// Нельзя: Func<byte> f = () => data[0];
}

Поддерживаемые типы:

  • ref T, ref readonly T
  • Span<T>, ReadOnlySpan<T>
  • scoped ref struct (в параметрах)

field (C# 12) — доступ к неявному полю автосвойства:

public string Name
{
get => field?.ToUpperInvariant() ?? "";
init => field = value?.Trim() ?? "";
}

→ Работает только в get/set/init, не в обычных свойствах с явным полем.


📚 Асинхронность

1. Основные типы асинхронных операций

ТипПрименениеАллокацииПримечания
TaskАсинхронная операция без результата1 (при первом await)Кэшированная Task.CompletedTask — использовать вместо new Task()
Task<T>Асинхронная операция с результатом T1 (при первом await)Если T — значимый тип, упаковка при await
ValueTaskОптимизация для синхронного случая0 (если синхронно завершено)Обёртка: либо Task, либо ManualResetValueTaskSourceCore<T>
ValueTask<T>То же, с результатом0 (если синхронно)Предпочтительно для hot-путей и интерфейсов (например, IAsyncEnumerable.MoveNextAsync())
IAsyncEnumerable<T>Асинхронный поток значений1–N (в зависимости от реализации)await foreach, yield return await, ConfigureAwait(false) в цикле
ConfiguredCancelableAsyncEnumerable<T>IAsyncEnumerable<T> с ConfigureAwait и CancellationTokenЧерез .ConfigureAwait(false).WithCancellation(ct)

Когда использовать ValueTask вместо Task:

  • Операция часто завершается синхронно (например, кэш hit).
  • Метод вызывается в hot-path (более 10⁶ вызовов/сек).
  • Интерфейс допускает ValueTask (например, IAsyncDisposable.DisposeAsync() возвращает ValueTask).

Ограничения ValueTask:

  • Нельзя await дважды (кроме .AsTask()).
  • Нельзя использовать Task.Wait(), .Result, .GetAwaiter().GetResult() без проверки.
  • Не поддерживает Task.WhenAll, Task.WhenAny напрямую — конвертировать через .AsTask().

2. async/await: синтаксис и семантика

Синтаксис:

public async Task<int> DownloadAndParseAsync(string url, CancellationToken ct = default)
{
using var client = new HttpClient();
string json = await client.GetStringAsync(url, ct).ConfigureAwait(false);
return JsonSerializer.Deserialize<int>(json);
}

Ключевые правила:

ПравилоПримечание
async метод должен содержать как минимум один await, или возвращать завершённую задачу напрямуюИначе предупреждение IDE0078 (но компилируется)
await распаковывает Task<T>T, ValueTask<T>T, Task/ValueTaskvoidИсключение пробрасывается напрямую (не завёрнуто в AggregateException)
await захватывает SynchronizationContext по умолчаниюВ UI — возвращает в поток UI; в ASP.NET — в контекст запроса
ConfigureAwait(false) — отменяет захват контекстаРекомендуется в библиотеках, особенно в цепочках await
async void — только для event handlersВ остальных случаях — async Task (иначе невозможно await, обработка исключений — через AppDomain.UnhandledException)

State machine (IL):

  • Компилятор генерирует struct-машину состояний:
    • Поля: параметры метода, локальные переменные, awaiter, текущее состояние.
    • Метод MoveNext() — реализует логику после await.
  • Размер state machine = сумма размеров захваченных переменных (оптимизация: ref struct в локальных async функциях в C# 12+).

3. CancellationToken и отмена операций

Создание:

// Одноразовый токен
var cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

// С таймаутом
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

// Комбинированный токен
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct1, ct2);

Использование:

public async Task<string> FetchAsync(CancellationToken ct)
{
ct.ThrowIfCancellationRequested(); // до дорогих операций

using var client = new HttpClient();
return await client.GetStringAsync("https://...", ct).ConfigureAwait(false);
// большинство .NET API поддерживают ct
}

Ручная проверка:

while (!ct.IsCancellationRequested)
{
await Task.Delay(100, ct).ConfigureAwait(false);
// или:
ct.ThrowIfCancellationRequested();
}

Обработка исключения:

try
{
await operation(ct);
}
catch (OperationCanceledException ex) when (ex.CancellationToken == ct)
{
// Корректная отмена — не ошибка
return default;
}

Важно:

  • Передавать CancellationToken явно во все асинхронные вызовы.
  • Не игнорировать ct в циклах и долгих синхронных операциях.
  • Использовать CancellationToken.Register только если нет async-альтернативы (дорого: аллокация делегата).

4. IAsyncEnumerable<T> и асинхронные потоки

Создание:

public async IAsyncEnumerable<int> GeneratePrimesAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 2; ; i++)
{
ct.ThrowIfCancellationRequested();
if (IsPrime(i))
yield return i;
await Task.Yield(); // добровольная уступка
}
}

Использование:

await foreach (int prime in GeneratePrimesAsync(ct)
.ConfigureAwait(false)
.WithCancellation(ct))
{
Console.WriteLine(prime);
if (prime > 1000) break;
}

Особенности:

  • [EnumeratorCancellation] — внедряет CancellationToken в метод (если вызван через .WithCancellation(ct)).
  • ConfigureAwait(false) и WithCancellation(ct) — chainable.
  • yield return await — разрешено:
    await foreach (var item in source)
    yield return await TransformAsync(item, ct);
  • IAsyncEnumerator<T> реализует IAsyncDisposableawait using.

Производительность:

  • Channel<T> предпочтительнее, если нужна очередь producer/consumer.
  • AsyncEnumerable — для ленивых, pull-based потоков.

5. Продвинутые механизмы: IValueTaskSource, ManualResetValueTaskSourceCore

Когда нужно:

  • Реализация async-операций без аллокаций даже при асинхронном завершении.
  • Высокопроизводительные библиотеки (сетевые протоколы, сериализаторы).

Пример:

public readonly struct MyValueTaskSource : IValueTaskSource<int>
{
private ManualResetValueTaskSourceCore<int> _core;

public ValueTask<int> GetValueAsync()
{
var vt = new ValueTask<int>(this, _core.Version);
// ... запуск операции, по завершении: _core.SetResult(value)
return vt;
}

// Реализация IValueTaskSource<int>:
public int GetResult(short token) => _core.GetResult(token);
public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
public void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags)
=> _core.OnCompleted(continuation, state, token, flags);
}

ManualResetValueTaskSourceCore<T>:

  • Внутренний state machine, оптимизированный для повторного использования.
  • Поддерживает SetResult, SetException, Reset.
  • Не потокобезопасен — требуется внешняя синхронизация.

6. Вспомогательные типы и методы

Тип / МетодНазначение
Task.CompletedTaskЗавершённая Task (кэширована) — использовать вместо Task.FromResult(0)
Task.FromResult<T>(T)Завершённая Task<T>
Task.FromException<T>(Exception)Завершённая с ошибкой
Task.FromCanceled<T>(CancellationToken)Отменённая задача
Task.Yield()Возвращает управление в синхронизатор (полезно в UI для прогресса)
Task.Delay(TimeSpan, CancellationToken)Асинхронная задержка с отменой
Task.WhenAll(params Task[])Ожидание всех, возвращает Task/Task<T[]>
Task.WhenAny(params Task[])Возвращает первую завершённую
TaskCompletionSource<T>Вручную управляемая задача (для оборачивания событий, callback-API)
PeriodicTimer (.NET 6+)await timer.WaitForNextTickAsync(ct) — замена Timer + TaskCompletionSource
Task.WaitAsync(TimeSpan, CancellationToken) (.NET 6+)Таймаут без CancellationTokenSource

Пример TaskCompletionSource:

var tcs = new TaskCompletionSource<bool>();
someEvent += (_, args) => tcs.SetResult(args.Success);
await tcs.Task;

7. Паттерны и анти-паттерны

✅ Рекомендованные:

  • Async all the way down — не смешивать async и .Result/.Wait().
  • ConfigureAwait(false) в библиотеках — избегать захвата контекста.
  • Передача CancellationToken явно — не использовать CancellationToken.None по умолчанию в публичных API.
  • Использовать ValueTask в интерфейсах, если реализация может быть синхронной.
  • IAsyncDisposable + await using — для асинхронного освобождения.

❌ Анти-паттерны:

  • async void вне event handlers.
  • .Result или .Wait() в async-методах ⇒ deadlock (в presence of SynchronizationContext).
  • Игнорирование CancellationToken.
  • Task.Run в библиотеках без необходимости (нарушает планирование).
  • new Task(...).Start() — предпочтительно Task.Run или Task.Factory.StartNew с явным TaskScheduler.

Безопасное синхронное ожидание (только в крайних случаях):

var task = asyncMethod();
var result = Task.Run(() => task).GetAwaiter().GetResult();
// или (в .NET 5+):
var result = task.AsTask().GetAwaiter().GetResult();

→ Избегает deadlock, но всё равно блокирует поток.


8. Современные инструменты (.NET 6–8)

ФичаПрименение
IProgress<T> + Progress<T>Отчёт о прогрессе из async-методов (автоматически маршрутизирует в UI-поток)
Channel<T>Producer/consumer с поддержкой async (Channel.CreateBounded, CreateUnbounded)
AsyncLock (из Nito.AsyncEx)Асинхронная блокировка (вместо lock)
SemaphoreSlim.WaitAsyncАсинхронное ограничение параллелизма
Parallel.ForEachAsync (.NET 6)Асинхронный параллелизм с контролем степени параллелизма

Channel<T> пример:

var channel = Channel.CreateBounded<int>(10);
_ = Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync(ct))
Process(item);
});
await channel.Writer.WriteAsync(42, ct);

📚 Метапрограммирование

1. Атрибуты: синтаксис и применение

Базовый синтаксис:

[<целевой_объект>:] <ИмяАтрибута>(<позиционные_параметры>, <именованные> = <значение>)

Целевые объекты (target):

ЦельПрименяется к
assemblyusing-директиве (в начале файла) — атрибут сборки
moduleтоже — атрибут модуля (редко)
fieldполю (например, в автосвойстве)
eventсобытию
methodметоду, конструктору, оператору, финализатору
paramпараметру метода
propertyсвойству
returnвозвращаемому значению
typevarпараметру типа (where T : [NotNull] object) — в обобщениях

Пример:

[assembly: AssemblyCopyright("© 2025")]

public string Name
{
[return: NotNull] // возвращаемое значение свойства
get => _name ??= "";

[param: AllowNull] // параметр set
set => _name = value;
}

2. Встроенные атрибуты .NET и C#

2.1. Управление видимостью и устареванием

АтрибутПараметрыЭффект
[Obsolete]message: string, error: bool = falseПредупреждение/ошибка компиляции при использовании
[EditorBrowsable(EditorBrowsableState.Never)]Скрывает из IntelliSense (не влияет на компиляцию)
[Browsable(false)]Для дизайнеров (WinForms/WPF)

2.2. Условная компиляция

АтрибутПараметрыЭффект
[Conditional("DEBUG")]conditionName: stringМетод и его вызовы удаляются, если символ не определён (#define DEBUG)
[Conditional("TRACE")]То же для System.Diagnostics.Trace

Важно: метод должен возвращать void и не иметь out-параметров (но допускает ref/in).

2.3. P/Invoke и нативный код

АтрибутПараметрыЭффект
[DllImport("kernel32")]EntryPoint, CharSet, CallingConvention, SetLastErrorИмпорт нативной функции
[MarshalAs(UnmanagedType.LPStr)]Указывает маршаллинг для параметра/поля/возврата
[StructLayout(LayoutKind.Sequential, Pack = 1)]Контроль макета памяти структуры
[SuppressGCTransition] (.NET 5+)Отключает переход GC-модуля для static extern методов (повышает производительность)

2.4. Оптимизации и JIT

АтрибутПараметрыЭффект
[MethodImpl(MethodImplOptions.AggressiveInlining)]Подсказка JIT встраивать метод
[MethodImpl(MethodImplOptions.NoInlining)]Запрет встраивания
[MethodImpl(MethodImplOptions.AggressiveOptimization)]Применять агрессивные оптимизации даже в debug
[ThreadStatic]static поле — по одному экземпляру на поток
[ContextStatic]Одна копия на ExecutionContext (устарел, AsyncLocal<T> предпочтительнее)
[AsyncMethodBuilder(typeof(MyBuilder))]Указывает кастомный билдер для async-методов

2.5. Nullable Reference Types (NRT) — аннотации

АтрибутПрименяется кЭффект
[NotNull]параметру, полю, свойствуПосле вызова значение точно не null
[MaybeNull]возвращаемому значениюМожет вернуть null, даже если тип не помечен как T?
[AllowNull]параметруРазрешает передавать null, даже если тип T (не T?)
[DisallowNull]параметруЗапрещает null, даже если тип T?
[NotNullWhen(true)]параметру out/refЕсли метод вернул true, то параметр не null
[MaybeNullWhen(false)]параметру out/refЕсли метод вернул false, параметр может быть null
[DoesNotReturn]методуМетод никогда не возвращается (например, throw new ...)
[DoesNotReturnIf(true)]параметру boolЕсли аргумент true, метод не возвращается
[MemberNotNull("Field1", "Property2")]методуПосле вызова указанные члены точно не null
[MemberNotNullWhen(true, "Field")]методуЕсли вернул true, член не null

Пример:

[return: MaybeNull]
public T GetOrDefault<T>(string key) { ... }

public bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value)
{
value = default;
// ...
return found;
}

2.6. Записи и обязательные члены (C# 11+)

АтрибутЭффект
[SetsRequiredMembers]Разрешает вызывать конструктор, не инициализируя required-члены — они будут установлены вызывающим кодом
[RequiredMember]Генерируется компилятором на типе с required-членами — не используется вручную

Пример:

public record Person(string Name, int Age);

[SetsRequiredMembers]
public static Person CreateAdmin() => new() { Name = "Admin", Age = 99 };

2.7. Интерполированные строки (C# 10–12)

АтрибутПрименениеЭффект
[InterpolatedStringHandler]ref structПозволяет реализовать кастомную обработку интерполированных строк без аллокаций
[InterpolatedStringHandlerArgument("sb")]Параметру обработчикаПередаёт дополнительные аргументы (например, StringBuilder)

Пример:

[InterpolatedStringHandler]
public ref struct LogHandler
{
public LogHandler(int literalLength, int formattedCount, StringBuilder sb) { ... }
public void AppendLiteral(string s) => sb.Append(s);
public void AppendFormatted<T>(T value) => sb.Append(value);
}

public static void Log([InterpolatedStringHandlerArgument("")] ref LogHandler handler);
// Использование: Log($"Value: {x}");

2.8. Инициализация и анализ

АтрибутЭффект
[ModuleInitializer]Статический метод без параметров — вызывается при загрузке модуля (до любого другого кода)
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(T))]Указывает linker'у, что тип T используется динамически — сохранить заданные члены
[UnscopedRef] (C# 12)Разрешает возврат ref из метода, помеченного scoped ref — отменяет ограничение

2.9. Compile-time проверки (.NET 7–8)

АтрибутПрименениеЭффект
[StringSyntax(StringSyntaxAttribute.Uri)]параметру stringПодсказывает анализаторам/IDE, что строка должна быть URI (IntelliSense, проверка в source generators)
[StringSyntax(StringSyntaxAttribute.Json)]Аналогично для JSON
[StringSyntax(StringSyntaxAttribute.Regex)]Проверка корректности регулярки на этапе компиляции
[Experimental("MY001")]типу/членуПомечает как экспериментальный — требует #pragma warning disable EXXX для использования
[ConstantExpected] (.NET 8)параметруПодсказка, что ожидается compile-time константа (анализатор может проверить)

Доступные StringSyntax:

  • Uri, Regex, Json, DateTimeFormat, TimeFormat, NumericFormat, CompositeFormat, MailAddress

3. Пользовательские атрибуты

Создание:

[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public sealed class MyAttribute : Attribute
{
public string Name { get; }
public int Priority { get; set; } = 0;

public MyAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}

Правила:

  • Должен наследоваться от System.Attribute.
  • Имя заканчивается на Attribute (но при применении суффикс можно опустить: [My]).
  • Позиционные параметры — только через конструктор.
  • Именованные — через публичные свойства/поля.
  • Параметры конструктора и свойств должны быть compile-time константами:
    • bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort, enum, Type, object, массивы этих типов.

Чтение в runtime (рефлексия):

var attr = typeof(MyClass).GetCustomAttribute<MyAttribute>();
if (attr?.Name == "Test") { ... }

Чтение в source generator (compile-time):

var attr = context.SemanticModel.GetDeclaredSymbol(typeSyntax)!.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.Name == "MyAttribute");
var name = attr?.ConstructorArguments[0].Value as string;

4. Рефлексия: основные API и производительность

Получение типов:

МетодПримечание
typeof(T)Compile-time, без аллокаций
obj.GetType()Runtime, возвращает RuntimeType
Type.GetType("Namespace.Type, Assembly")По строке (медленно, избегать в hot-path)

Создание экземпляров:

МетодПроизводительностьПримечание
new T()наилучшаяТолько для where T : new()
Activator.CreateInstance<T>()~10× медленнее newИспользует кэшированный Func<T> в .NET Core+
Activator.CreateInstance(type)~100× медленнееВозвращает object, упаковка для value types
System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(type)Очень быстроНе вызывает конструктор — только для сериализаторов

Вызов методов:

МетодПроизводительностьПримечание
Прямой вызов
MethodInfo.Invoke~1000× медленнееАллокации, проверки безопасности
Delegate.CreateDelegate + вызов~10× медленнееКэширование делегата критично
Expression.Lambda(...).Compile()~5× медленнееСоздаёт IL-код; CompileToMethod — для статики

Кэширование делегатов (рекомендуется):

private static readonly Func<MyClass, int> _getter =
(Func<MyClass, int>)Delegate.CreateDelegate(
typeof(Func<MyClass, int>),
typeof(MyClass).GetMethod("GetValue")!);

Альтернативы (без рефлексии):

  • interface + generic-ограничения.
  • source generators — генерация partial методов.
  • System.Text.Json.Serialization.Metadata — метаданные для сериализации без рефлексии.

5. Source Generators (C# 9+)

Архитектура:

[Generator]
public partial class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0,
transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Collect();

context.RegisterSourceOutput(provider, (spc, classes) =>
{
foreach (var c in classes)
spc.AddSource($"{c.Identifier}.g.cs", $$"""
partial class {{c.Identifier}}
{
public void GeneratedMethod() => Console.WriteLine("Hello");
}
""");
});
}
}

Incremental mode (предпочтительно):

  • SyntaxValueProvider.CreateSyntaxProvider — фильтрация и преобразование AST.
  • CompilationProvider — доступ к символам (SemanticModel).
  • Combine, Select, Collect, Where — композиция pipeline.
  • Автоматический кэш промежуточных результатов.

Триггеры генерации:

  • Изменение синтаксического дерева.
  • Изменение семантики (референсы, using'и).
  • Изменение дополнительных файлов (AdditionalText).

Генерация:

  • Только partial типы и методы.
  • Имена файлов: <имя>.<хэш>.g.cs — не должны конфликтовать.
  • Диагностика: context.ReportDiagnostic(Diagnostic.Create(...)).

Пример: генерация Deconstruct для класса с [GenerateDeconstruct]:

[ModuleInitializer]
internal static void Init()
{
// Регистрация анализатора
}

6. Директивы препроцессора

ДирективаПрименение
#define SYMBOLОпределить символ (только в начале файла)
#undef SYMBOLОтменить символ
#if SYMBOL / #elif / #else / #endifУсловная компиляция
#warning "text"Предупреждение компиляции
#error "text"Ошибка компиляции
#line 200 / #line default / #line hiddenУправление отладочной информацией (для генераторов)
#nullable enable / disable / restoreУправление NRT на уровне файла/блока

Стандартные символы:

  • DEBUG, TRACE — определяются в проекте.
  • NET6_0, NET7_0, NET8_0 — целевая платформа (автоматически).
  • WINDOWS, LINUX, BROWSER — ОС/среда (.NET 5+).

7. Caller-атрибуты (compile-time подстановка)

АтрибутПодставляет
[CallerFilePath]Путь к файлу вызова (string)
[CallerLineNumber]Номер строки (int)
[CallerMemberName]Имя метода/свойства (string)

Пример:

public void Log(string message,
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0)
{
Console.WriteLine($"{file}({line}): {member}{message}");
}

→ Вызов Log("test")"Program.cs(42): Main → test".